using System;
using System.Web.Services.Protocols;
using System.IO;
using System.Diagnostics;
using NZlib.Zip;

namespace WSSoapCompression
{
	/// <summary>
	/// Attribute class to provide compression of Soap Messasges
	/// </summary>
	public class WSCompress: SoapExtension
	{
		#region Privates
		// Initialiser object
		private CompressionLevels _compressionLevel = CompressionLevels.High;  // defaults to high

		private Stream _networkStream;
		private MemoryStream _bufferedStream;
		#endregion

		#region Constructor
		public WSCompress() {}
		#endregion

		#region Chainstream
		public override Stream ChainStream(Stream stream)
		{
			// The network stream passed to us by the runtime and is the "request" stream.
			_networkStream = stream;
			// This will be written to by the runtime during the serialise/deserialise process. We return a
			// local stream here so we can have a chance to manipulate/investigate the contents during the
			// different message processing stages. Given that we now have a local copy of this stream, we
			// will simply call different processes to operate on this stream during the message stages.
			_bufferedStream = new MemoryStream(); 
			return _bufferedStream;
		}
		#endregion

		#region ProcessMessage
		public override void ProcessMessage(SoapMessage message)
		{
			switch (message.Stage)
			{
				case SoapMessageStage.AfterDeserialize:
					// Server: Step 4
					// Client: Step 8
					Debug.WriteLine("SoapMessage.AfterDeserialise");
					break;
				case SoapMessageStage.AfterSerialize:
					// Client: step 2
					// Server: Step 6
					Debug.WriteLine("SoapMessage.AfterSerialise");
					// This is where we should compress the message body as its where the return of the
					// method call is is serialised into soap and sent back to client.
					Compress();
					break;
				case SoapMessageStage.BeforeDeserialize:
					// Server: step 3
					// Cliet: Step 7
					Debug.WriteLine("SoapMessage.BeforeDeserialise");
					// Should gather the message body and decompress here - this is where the call is coming
					// in from the client, just prior to deserialising into a method call.
					Decompress();
					break;
				case SoapMessageStage.BeforeSerialize:
					// Client: Step 1
					// Server: Step 5
					Debug.WriteLine("SoapMessage.BeforeSserialise");
					// before the message object is serialised into soap
					// Add in the compression header according to what was specified in the assembly via the WSCompression
					WSCompressHeader wsh = new WSCompressHeader();
					wsh.CompressionLevel = _compressionLevel;
					message.Headers.Add(wsh);
					break;
			}
		}
		#endregion

		#region Compression and Decompression routines
		/// <summary>
		/// Compress is always called at the AfterSerialize stage. After which the compressed (or perhaps uncompressed) data will
		/// be written to the wire, in this case the NetworkStream. So, we must ensure that all data gets written to the network
		/// stream. The network stream hasn't sent its data at this stage so we can write to it.
		/// </summary>
		private void Compress()
		{
			// Create a temporary memory stream
			MemoryStream tmpMem = new MemoryStream();

			if (_compressionLevel != CompressionLevels.None)
			{
				ZipOutputStream zip = new ZipOutputStream(tmpMem);
				zip.SetLevel(Convert.ToInt32(_compressionLevel));
				ZipEntry entry = new ZipEntry("SoapBody");
				zip.PutNextEntry(entry);
			
				// Buffered stream will contain what has been currently written to the stream by the runtime
				// so we write it to our temp stream for manipulation (can't manipulate the runtime stream).
				zip.Write(_bufferedStream.GetBuffer(),0,(int)_bufferedStream.Length);
				zip.Finish();

				string compStr = Convert.ToBase64String (tmpMem.GetBuffer (), 0, (int)tmpMem.Length);
				zip.Close();

				byte[] tmpB = System.Text.Encoding.UTF8.GetBytes(compStr);
			
			
				_networkStream.Write(tmpB,0,(int)tmpB.Length);
			} 
			else
			{
				tmpMem.Write(_bufferedStream.GetBuffer(),0,(int)_bufferedStream.Length);
				tmpMem.Flush();
				
				// Write our temporary memory stream to the network stream so that it gets transmitted
				_networkStream.Write(tmpMem.GetBuffer(),0,(int)tmpMem.Length);
				tmpMem.Close();
			}
		}

		/// <summary>
		/// Decompress is always called after the Deserialize stage, and contains the data that is passed to the consumer (it maybe
		/// the client or the server). As such, only the buffered stream gets passed to the consumer as the network stream has done its
		/// job of passing the data to us. Since the network stream has done its job, we can't write to the stream again as this doesn't
		/// make sense since the network has sent us the data and we are not responding at this stage.
		/// </summary>
		private void Decompress()
		{

			if (_compressionLevel != CompressionLevels.None)
			{
				// Create a stream reader to give us a string representation of the network stream
				StreamReader sr = new StreamReader(_networkStream,System.Text.Encoding.UTF8);
				string netStr = sr.ReadToEnd();
				sr.Close();

				// Get the encoded bytes from the Base64 string.
				byte[] tmpB = Convert.FromBase64String(netStr);

				// Make a tmporary memory stream to work on
				// (failure to do this i.e working directly on the network stream can have adverse effects)
				MemoryStream tmpMem = new MemoryStream(tmpB);

				// Use the Zip compression libraries to decompress the bytes
				ZipInputStream zip = new ZipInputStream(tmpMem);
				ZipEntry entry = zip.GetNextEntry();
				// We need to read in the compress stream chunks at a time. The 'Read' method of the
				// ZipInputStream can only read a max of 23191 bytes (I think thats its max)
				int size=1;
				byte[] buf = new byte[2048];
				System.Text.StringBuilder sb = new System.Text.StringBuilder();

				while (size > 0)
				{
					size = zip.Read(buf,0,2048);
					if (size > 0)
						sb.Append(System.Text.Encoding.UTF8.GetString(buf,0,size));
				}
				zip.Close();  // should implicitly close our memory stream.
			
				//Write our tnetwork stream to the buffered stream so that it gets transmitted
                byte[] bytesSB = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
                _bufferedStream.Write(bytesSB, 0, bytesSB.Length); 
			} 
			else
			{
				// No Compression, plain text
				StreamReader sr = new StreamReader(_networkStream);
				string s = sr.ReadToEnd();
				sr.Close();
				_bufferedStream.Write(System.Text.Encoding.UTF8.GetBytes(s),0,s.Length);
			}
			
			_bufferedStream.Position = 0;  // If we dont do this, nothing is returned!
			
		}
		#endregion

		#region Initialise method overrides
		/// <summary>
		/// Called once when this Soap Extension is initialised. This is only called once when the extension
		/// is initialised.
		/// </summary>
		/// <param name="methodInfo"></param>
		/// <param name="attribute"></param>
		/// <returns></returns>
		public override object GetInitializer(LogicalMethodInfo methodInfo,	SoapExtensionAttribute attribute) 
		{
			CompressionLevels level = ((WSCompression)attribute).CompressionLevel;
			return level;
		}

		/// <summary>
		/// Called each time this Soap Extension is initialised. This is only called once when the 
		/// Soap Extension is initialised.
		/// </summary>
		/// <param name="type"></param>
		/// <returns></returns>
		public override object GetInitializer(Type type)
		{
			return null;
		}

		/// <summary>
		/// Provides an initialisation routine that is called on each time a Soap request is made. Gets the object that
		/// is cached via the GetInitialiser method.
		/// </summary>
		/// <param name="initializer"></param>
		public override void Initialize(object initializer)
		{
			// Sets our internval values
			_compressionLevel = (CompressionLevels)initializer;
		}

		#endregion

	}
}
